/**
 * *****************************************************************************
 * Copyright (c) 2011 GigaSpaces Technologies Ltd. All rights reserved
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 * *****************************************************************************
 */
package com.kedacom.demo.sigar.cloudify;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.math.NumberUtils;
import org.hyperic.sigar.FileInfo;
import org.hyperic.sigar.ProcState;
import org.hyperic.sigar.Sigar;
import org.hyperic.sigar.SigarException;




import com.kedacom.demo.sigar.ServiceUtils;
import com.kedacom.demo.sigar.util.SigarUtil;

/**
 * *******
 * Utility class for the USM package.
 *
 * @author barakme
 *
 */
public final class USMUtils {

    // private static final int POST_SYNC_PROCESS_SLEEP_INTERVAL = 500;
    private USMUtils() {
        // private constructor to prevent instantiation.
    }
    private static java.util.logging.Logger logger = java.util.logging.Logger.getLogger(USMUtils.class.getName());

    /**
     * ******
     * Marks a file as executable by the operating system. On windows, always
     * returns true. Otherwise, attempts will be made to mark the file as
     * executable using Java 6 API, if possible. If not, will attempt to use
     * sigar. If this fails, a chmod command will be executed.
     *
     * @param sigar an instance of sigar.
     * @param file the file to make as executable.
     * @return true if the file was successfully marked as executable, false
     * otherwise.
     * @throws USMException in case of an error.
     */
    public static boolean markFileAsExecutable(final Sigar sigar, final File file)
            throws USMException {

        if (!file.exists()) {
            return false;
        }
        if (ServiceUtils.isWindows()) {
            return true; // not used in windows
        }

        if (USMUtils.markFileAsExecutableUsingJdk(file)) {
            return true;
        }

        if (USMUtils.isFileExecutableSigar(file)) {
            return true;
        }

        logger.info("File " + file + " is not executable according to sigar");
        return USMUtils.markFileAsExecutableUsingChmod(file);
    }

    private static boolean markFileAsExecutableUsingJdk(final File file)
            throws USMException {
        // TODO - refactor this - just catch exception on the whole block
        logger.fine("Attempting to set file as executable using REFLECTION");
        Method method = null;
        try {
            method = File.class.getMethod("setExecutable", new Class[]{Boolean.TYPE});
        }
        catch (final SecurityException e) {
            throw new USMException("Failed to check if file " + file.getAbsolutePath() + " is executable", e);
        }
        catch (final NoSuchMethodException e) {
            // ignore - this can happen
        }

        if (method == null) {
            logger.warning("setExecutable not supported by JDK. "
                    + "GigaSpaces Cloudify should be executed on Java Version >= 1.6");
            return false;
        }

        try {
            final boolean retval = (Boolean) method.invoke(file, true);
            logger.fine("File " + file + " was marked as executable");
            if (!retval) {
                logger.warning("File: " + file + " is not executable. "
                        + "Attempt to mark it as executable has failed. "
                        + "Execution of this command may not be possible");
            }
            return retval;
        }
        catch (final IllegalArgumentException e) {
            throw new USMException("Failed to make file " + file.getAbsolutePath() + " as executable", e);
        }
        catch (final IllegalAccessException e) {
            throw new USMException("Failed to make file " + file.getAbsolutePath() + " as executable", e);
        }
        catch (final InvocationTargetException e) {
            throw new USMException("Failed to make file " + file.getAbsolutePath() + " as executable", e);
        }
    }

    private static boolean isFileExecutableSigar(final File file)
            throws USMException {
        long mode;
        try {
            logger.info("Checking if file " + file + " is executable using sigar");
            mode = SigarUtil.getSigarInstance().getFileInfo(file.getAbsolutePath()).getMode();
        }
        catch (final SigarException e) {
            throw new USMException("Failed to check if file " + file.getAbsolutePath() + " is executable", e);
        }

        final long executable = mode & FileInfo.MODE_UEXECUTE;
        return executable > 0;

    }

    private static boolean markFileAsExecutableUsingChmod(final File file)
            throws USMException {
        try {
            logger.info("File " + file + " will be marked as executable using chmod command");
            final Process p = Runtime.getRuntime().exec(new String[]{"chmod", "+x", file.getAbsolutePath()});
            final int result = p.waitFor();
            logger.info("chmod command finished with result = " + result);
            if (result != 0) {
                final String msg =
                        "chmod command did not terminate successfully. File " + file + " may not be executable!";
                logger.warning(msg);
            }
            return result == 0;
        }
        catch (final IOException e) {
            throw new USMException("Failed to execute chmod command:  chmod +x " + file.getAbsolutePath(), e);
        }
        catch (final InterruptedException e) {
            throw new USMException("Failed to execute chmod command on file " + file, e);
        }
    }

    /**
     * *******
     * Returns the list of parent processes, starting by the child pid and
     * ending with the current pid.
     *
     * @param childPid the list starting point.
     * @param myPid the curennt process pid.
     * @return the pid list.
     * @throws USMException in case of an error.
     */
    public static List<Long> getProcessParentChain(final long childPid, final long myPid)
            throws USMException {

        final LinkedList<Long> list = new LinkedList<Long>();
        if (childPid == 0) {
            logger.warning("The child PID is 0 - this usually means that this USM instance had a problem "
                    + "during startup. Please check the logs for more details.");
            return list;
        }

        long currentPid = childPid;

        while (currentPid != myPid) {
            list.add(currentPid);
            final long ppid = USMUtils.getParentPid(currentPid);

            if (ppid == 0) {
                logger.severe("Attempt to create a process chain from child process " + childPid + " this process("
                        + myPid + "). Process " + childPid + " is not a descendant of this process. Only process "
                        + childPid + " will be included in the chain");
                list.clear();
                list.add(childPid);
                return list;
            }
            currentPid = ppid;
        }

        return list;
    }

    /**
     * ****
     * Returns the parent pid for the given pid.
     *
     * @param pid the process pid.
     * @return the parent pid for the given pid.
     * @throws USMException in case of an error.
     */
    public static long getParentPid(final long pid)
            throws USMException {
        ProcState procState = null;

        procState = USMUtils.getProcState(pid);
        if (procState == null) {
            return 0;
        }

        return procState.getPpid();

    }

    /**
     * *******
     * Checks, using Sigar, is a given process is alive.
     *
     * @param pid the process pid.
     * @return true if the process is alive (i.e. not stopped or zombie).
     * @throws USMException in case of an error.
     */
    public static boolean isProcessAlive(final long pid)
            throws USMException {

        final ProcState procState = USMUtils.getProcState(pid);
        return (procState != null && procState.getState() != ProcState.STOP && procState.getState() != ProcState.ZOMBIE);
    }

    private static ProcState getProcState(final long pid)
            throws USMException {
        final Sigar sigar = SigarUtil.getSigarInstance();

        ProcState procState = null;
        try {
            procState = sigar.getProcState(pid);
        }
        catch (final SigarException e) {
            if ("No such process".equals(e.getMessage())) {
                return null;
            }

            throw new USMException("Failed to check if process with PID: " + pid + " is alive. Error was: "
                    + e.getMessage(), e);
        }
        return procState;
    }

    /**
     * *********
     * Given a map, returns a new map where the values are numbers, matching
     * entries from the original list which were numbers or strings that can be
     * parsed as numbers. Other entries are discarded.
     *
     * @param map the original map.
     * @return the numbers map.
     */
    // converts a map of type <String, Object> to a Map of <String, Number>
    public static Map<String, Number> convertMapToNumericValues(final Map<String, Object> map) {
        final Map<String, Number> returnMap = new HashMap<String, Number>();
        for (final Map.Entry<String, Object> entryObject : map.entrySet()) {
            if (entryObject.getValue() instanceof Number) {
                returnMap.put(entryObject.getKey(), (Number) entryObject.getValue());
            } else if (entryObject.getValue() instanceof String) {
                if (NumberUtils.isNumber((String) entryObject.getValue())) {
                    final Number number = NumberUtils.createNumber((String) entryObject.getValue());
                    returnMap.put(entryObject.getKey(), number);
                }
            } else {
                logger.info("monitor value is expected to be numeric: " + entryObject.getValue().toString());
            }
        }
        return returnMap;
    }

    /**
     * *******
     * Returns the exit code of the given process handle, or null if the process
     * has not terminated.
     *
     * @param processToCheck the process.
     * @return the exit code, or null.
     */
    public static Integer getProcessExitCode(final Process processToCheck) {
        if (processToCheck == null) {
            return null;
        }
        try {
            int val = processToCheck.exitValue();
            return val;
        }
        catch (final Exception e) {
            return null;
        }
    }
}
